#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the version 3 of the GNU Lesser General Public License
#   as published by the Free Software Foundation.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Copyright (c) NEC Deutschland GmbH, NEC HPC Europe
#
# $Id$
import logging
from time import time
from basic_types.metric import Metric


__all__ = [ "MetricCache" ]


class HostState:
    """
    Contains the cached information for each host.
    """
    def __init__( self, last_seen=0, marshaller=None, unmarshaller=None ):
        self.last_seen = last_seen
        self.marshaller = marshaller
        self.unmarshaller = unmarshaller
        self.metrics = {}

    def get_metric( self, metric_name ):
        if metric_name in self.metrics:
            return self.metrics[metric_name]["last"]
        return None

    def set_metric( self, metric, ts_handle=None ):
        if metric.name in self.metrics:
            self.metrics[metric.name]["last"] = metric
            if ts_handle is not None:
                self.metrics[metric.name]["ts_handle"] = ts_handle
        else:
            self.metrics[metric.name] = { "last" : metric, "ts_handle" : ts_handle }

    def get_last_seen( self ):
        return self.last_seen

    def set_last_seen( self, last_seen ):
        self.last_seen = last_seen

    def get_marshaller_unmarshaller( self ):
        return (self.marshaller, self.unmarshaller)

    def set_marshaller_unmarshaller( self, marshaller=None, unmarshaller=None ):
        if marshaller is not None:
            self.marshaller = marshaller
        if unmarshaller is not None:
            self.unmarshaller = unmarshaller

    def get_ts_handle( self, metric_name ):
        if metric_name in self.metrics:
            return self.metrics[metric_name]["ts_handle"]
        return None

    def set_ts_handle( self, metric_name, ts_handle ):
        if metric_name in self.metrics:
            self.metrics[metric_name]["ts_handle"] = ts_handle
        else:
            self.metrics[metric_name] = { "last" : None, "ts_handle" : ts_handle }


class MetricCache:
    """
    Purpose: Cache LAST metric for efficient append operation in MetricDatabase.
    This class maintains a dictionary indexed by the host names for
    which data is stored within this database instance. The value of each dict
    entry is a dictionary of the latest metrics known for the particular
    host. {host_name : {metric_name : Metric, ...}, ...}
    """
    def __init__( self, db=None ):
        self.__db = db

        # the host_state dictionary contains all cached information
        self.host_state = {}

    def load_state( self ):
        for host_name in sorted( self.__db.local_getHostNames() ):
            host_metrics = sorted( self.__db.local_getLastMetricsByHostName( host_name ),
                                   key = lambda metric: metric.name )
            self.host_state[host_name] = HostState()
            for metric in host_metrics:
                self.host_state[host_name].set_metric( metric )
                if hasattr( metric, "no_log" ):
                    self.host_state[host_name].set_ts_handle( metric.name, "NOTS" )
                    return
                try:
                    ts_handle = self.__db._initTimeSeriesInfo( host_name, metric.name )
                except Exception, e:
                    logging.error( "Failed to init ts_info for metric %s, host %s: %s" % (metric.name, host_name, str(e)) )
                else:
                    self.host_state[host_name].set_ts_handle( metric.name, ts_handle )

    def test_append_and_strip( self, metric ):
        host_name = metric.host
        metric_name = metric.name
        #
        # Mark when we've seen anything from the host the last time.
        # The timestamp shall be used as an implicit heartbeat. It could become a real heartbeat if we
        # have some periodically changing metric, like the ganglia heartbeat. Though the time values might be
        # different (timestamp_last_seen: group master time, ganglia.heartbeat.value: host time).
        #
        new = False
        if not host_name in self.host_state:
            self.add_host_state( host_name, last_seen=time() )
            new = True
        if not metric_name in self.host_state[host_name].metrics:
            new = True
        if new:
            self.host_state[host_name].set_metric( metric )
            return "append", metric, {}
        else:
            old_metric = self.host_state[host_name].get_metric( metric_name )
            try:
                if not new and old_metric.time <= metric.time:
                    stripped_metric = self.strip_metric( old_metric, metric )
                    self.host_state[host_name].set_metric( metric )
                    return "append", stripped_metric, old_metric
                else:
                    if host_name != metric.host or host_name != old_metric.host:
                        logging.error( "name mismatch: host_name=%s metric.host=%s old_metric.host=%s" % (host_name, metric.host, old_metric.host))
                    return "insert", metric, old_metric
            except Exception, e:
                logging.error( "Exception on metric %s" % repr(metric) )
                logging.error( "or on last metric %s" % repr(old_metric) )
                raise e

    def contains_metrics_of_host( self, host_name ):
        return host_name in self.host_state

    def contains_metric_with_name( self, host_name, metric_name ):
        return metric_name in self.host_state[host_name].metrics

    def dump( self ):
        for host_name in self.host_state.keys():
            host_state = self.host_state[host_name]
            for metric_name in host_state.metrics:
                metric = host_state.get_metric( metric_name )
                print host_name, metric_name, repr( metric )

    def locate_metric_condition( self, metric_name, metric_attr, condition, value ):
        group_path = self.__db._group_path
        result = {}
        for host_name in self.host_state.keys():
            if not metric_name in self.host_state[host_name].metrics:
                continue
            metric = self.host_state[host_name].get_metric( metric_name )
            if not metric_attr in metric.keys():
                continue
            attr = metric.getValue( metric_attr )
            if isinstance(attr, float):
                value = float(value)
            elif isinstance(attr, int):
                value = int(value)
            elif isinstance(attr, long):
                value = long(value)
            host_path = group_path + (group_path.endswith( "/" ) and host_name or "/" + host_name)
            # default: value is passed in as string
            if ((condition == "==") and (attr == value)) or \
                ((condition == ">") and (attr > value)) or \
                ((condition == "<") and (attr < value)) or \
                ((condition == ">=") and (attr >= value)) or \
                ((condition == "<=") and (attr <= value)) or \
                ((condition == "!=") and (attr != value)):
                result[host_path] = metric
        return result

    def last_seen( self, host_name ):
        "Return when the host was last seen: time stamp and age"
        now = time()
        if host_name in self.host_state:
            timestamp = self.host_state[host_name].get_last_seen()
            return (timestamp, now - timestamp)
        return (0, now)

    def strip_metric( self, old, new ):
        stripped = {}
        for key in new.keys():
            if not key in old.keys() or \
                old.getValue( key ) != new.getValue( key ) or \
                key == "time" or key == "value":
                stripped[key] = new.getValue( key )
        return Metric( **stripped )

    def add_host_state( self, host_name, **kwds ):
        hs = HostState( **kwds )
        self.host_state[host_name] = hs
        return hs

    def get_host_state( self, host_name ):
        if not host_name in self.host_state:
            self.add_host_state( host_name )
        return self.host_state[host_name]

    def get_hosts_in_cache( self ):
        return self.host_state.keys()
